home *** CD-ROM | disk | FTP | other *** search
/ Magnum One / Magnum One (Mid-American Digital) (Disc Manufacturing).iso / d18 / opbonus.arc / DUPFIND.ARC / DUPFIND.PAS next >
Pascal/Delphi Source File  |  1991-03-20  |  15KB  |  557 lines

  1. {************************************************************************
  2.  Find all files with duplicate names on specified drive(s) and write a list
  3.  of them. Run DUPFIND without command line parameters to get help.
  4.  
  5.  Requires the commercial product Object Professional to compile.
  6.  
  7.  DUPFIND provides the following general-purpose objects:
  8.    Tree - a binary tree (in OPTREE.PAS)
  9.    WildMatcher - a string matcher that understands * and ?
  10.  
  11.  Written 1/13/90, Kim Kokkonen, TurboPower Software
  12.  CompuServe ID [76004,2611]
  13. ************************************************************************}
  14.  
  15. {$R-,S-,I-,V-,B-,F-}
  16.  
  17. program DupFind;
  18.   {-Find duplicate files}
  19.  
  20. uses
  21.   Dos, OpString, OpDos, OpRoot, OpTree;
  22.  
  23. const
  24.   FileAttr = AnyFile;         {File attributes being checked}
  25.  
  26. type
  27.   FileStr = String[12];
  28.  
  29.   DirNodePtr = ^DirNode;
  30.   DirNode =
  31.     object(SingleListNode)
  32.       Time : Longint;         {Date/time stamp of file in this directory}
  33.       Size : Longint;         {Size of file in this directory}
  34.       DNameP : StringPtr;     {Pointer to name of directory}
  35.       constructor Init(FileTime, FileSize : Longint; Dirname : PathStr);
  36.         {-Initialize a DirNode}
  37.       destructor Done; virtual;
  38.         {-Destroy a DirNode}
  39.     end;
  40.  
  41.   FileNodePtr = ^FileNode;
  42.   FileNode =
  43.     object(TreeNode)
  44.       NameP : StringPtr;      {Pointer to name of file}
  45.       DirList : SingleList;   {List of DirNodes}
  46.       constructor Init(FileName : FileStr);
  47.         {-Initialize a FileNode}
  48.       destructor Done; virtual;
  49.         {-Destroy a FileNode}
  50.     end;
  51.  
  52.   FileTreePtr = ^FileTree;
  53.   FileTree =
  54.     object(Tree)
  55.       ftUnique : LongInt;     {Number of uniquely named files in tree}
  56.       ftTotal : LongInt;      {Total number of files in tree}
  57.       constructor Init;
  58.         {-Initialize FileTree}
  59.       procedure FileInsert(FileName : FileStr;
  60.                            FileTime, FileSize : LongInt;
  61.                            Dirname : PathStr);
  62.         {-Add a file and directory name to the tree list}
  63.       procedure DumpDups;
  64.         {-Dump files that appear at least twice}
  65.       procedure GetCounts(var Unique, Total : LongInt);
  66.         {-Return the number of unique names and the total number of names}
  67.       {-- override virtual methods required by Tree object --}
  68.       function Compare(Key1, Key2 : Pointer) : CompareType; virtual;
  69.         {-Compare two keys, returning Less, Equal, Greater}
  70.       function GetKey(N : TreeNodePtr) : Pointer; virtual;
  71.         {-Return a pointer to the key value for node N}
  72.     end;
  73.  
  74. const
  75.   AnyChar = '*';         {Match zero or more characters}
  76.   OneChar = '?';         {Match exactly one character}
  77.   EndChar = #255;        {Terminator to match strings}
  78.  
  79. type
  80.   WildMatcherPtr = ^WildMatcher;
  81.   WildMatcher =
  82.     object(Root)
  83.       maCase : Boolean;       {True if case-sensitive matching}
  84.       maMask : String[128];   {Mask used for matching}
  85.       constructor Init(Mask : String; CaseSensitive : Boolean);
  86.         {-Initialize the mask string. May fail if Mask is invalid}
  87.       function Matches(Name : String) : Boolean;
  88.         {-Return True if Name matches Mask}
  89.       function GetMask : String;
  90.         {-Return the simplified mask}
  91.       procedure SimplifyMask;
  92.         {-Used internally to simplify mask when object instantiated}
  93.     end;
  94.  
  95. var
  96.   StdErr : Text;              {File where messages are written}
  97.   FileNames : FileTree;       {Stores all the files}
  98.   FileMask : WildMatcher;     {Used for wildcard matching}
  99.   DefaultMask : Boolean;      {True if mask is *.* and FileMask isn't used}
  100.  
  101. procedure WriteCopyright;
  102. begin
  103.   WriteLn(StdErr,
  104.   'DUPFIND 1.0 - Duplicate file finder. Copyright (c) TurboPower Software, 1990'^M^J);
  105. end;
  106.  
  107. procedure WriteHelp;
  108.   {-Write a help message and halt}
  109. begin
  110.   WriteLn('Usage: DUPFIND Drive [Drive ...] [/S FileMask] [>OutputRedirection]');
  111.   WriteLn;
  112.   WriteLn('DUPFIND creates an alphabetized list of all duplicate files on the');
  113.   WriteLn('specified disk drives. One or more valid drive letters must be given.');
  114.   WriteLn;
  115.   WriteLn('By default, DUPFIND scans all files on the drives. Use the /S option');
  116.   WriteLn('to limit the search. FileMask is an exact filename or an extended DOS');
  117.   WriteLn('wildcard pattern like *.PAS or OP??????.* or even ARC*X.*');
  118.   WriteLn;
  119.   WriteLn('DUPFIND writes a report to standard output. The report looks like the');
  120.   WriteLn('following:');
  121.   WriteLn('  README.COM');
  122.   WriteLn('        4217 89/06/28 05:50:00 c:\t55\rtl');
  123.   WriteLn('        4200 88/08/29 05:00:00 c:\t5');
  124.   WriteLn('  README.DOC');
  125.   WriteLn('       18520 89/04/25 10:01:04 c:\word');
  126.   WriteLn('        2405 89/05/05 10:14:24 c:\qp');
  127.   WriteLn('which shows the size, date, time, drive, and directory of the dup files.');
  128.   WriteLn;
  129.   WriteLn('The following command line');
  130.   WriteLn('  DUPFIND C D E /S READ*.* >JUNK');
  131.   WriteLn('searches drives C, D, and E for all files starting with READ');
  132.   Halt;
  133. end;
  134.  
  135. procedure Abort(Msg : String);
  136.   {-Report error message and abort}
  137. begin
  138.   WriteLn(StdErr, Msg);
  139.   Halt(1);
  140. end;
  141.  
  142. procedure InsufficientMemory;
  143.   {-Report insufficient memory and abort}
  144. begin
  145.   Abort('Insufficient memory');
  146. end;
  147.  
  148. constructor DirNode.Init(FileTime, FileSize : Longint; Dirname : PathStr);
  149.   {-Initialize the node}
  150. begin
  151.   if not SingleListNode.Init then
  152.     Fail;
  153.   DNameP := nil;
  154.   if not GetMemCheck(DNameP, Length(Dirname)+1) then begin
  155.     Done;
  156.     Fail;
  157.   end;
  158.   DNameP^ := Dirname;
  159.   Time := FileTime;
  160.   Size := FileSize;
  161. end;
  162.  
  163. destructor DirNode.Done;
  164.   {-Destroy the node}
  165. begin
  166.   if DNameP <> nil then
  167.     FreeMem(DNameP, Length(DNameP^)+1);
  168.   SingleListNode.Done;
  169. end;
  170.  
  171. constructor FileNode.Init(FileName : FileStr);
  172.   {-Initialize a FileNode}
  173. begin
  174.   if not TreeNode.Init then
  175.     Fail;
  176.   NameP := nil;
  177.   if not(DirList.Init and GetMemCheck(NameP, Length(FileName)+1)) then begin
  178.     Done;
  179.     Fail;
  180.   end;
  181.   NameP^ := FileName;
  182. end;
  183.  
  184. destructor FileNode.Done;
  185.   {-Destroy a FileNode}
  186. begin
  187.   if NameP <> nil then
  188.     FreeMem(NameP, Length(NameP^)+1);
  189.   DirList.Done;
  190. end;
  191.  
  192. constructor FileTree.Init;
  193.   {-Initialize FileTree}
  194. begin
  195.   ftUnique := 0;
  196.   ftTotal := 0;
  197.   if not Tree.Init then
  198.     Fail;
  199. end;
  200.  
  201. procedure FileTree.FileInsert(FileName : FileStr;
  202.                               FileTime, FileSize : LongInt;
  203.                               Dirname : PathStr);
  204.   {-Add a file and directory name to the tree list}
  205. var
  206.   DirIndex : Word;
  207.   FileNP : FileNodePtr;
  208.   DNodeP : DirNodePtr;
  209. begin
  210.   {See if filename is already in tree}
  211.   FileNP := FileNodePtr(Find(@FileName));
  212.   if FileNP = nil then begin
  213.     {Insert filename in tree}
  214.     New(FileNP, Init(FileName));
  215.     if FileNP = nil then
  216.       InsufficientMemory;
  217.     Insert(FileNP);
  218.     Inc(ftUnique);
  219.   end;
  220.  
  221.   {Create a directory node to add to the dictionary}
  222.   New(DNodeP, Init(FileTime, FileSize, Dirname));
  223.   if DNodeP = nil then
  224.     InsufficientMemory;
  225.  
  226.   {Add directory node to list}
  227.   FileNP^.DirList.Append(DNodeP);
  228.  
  229.   inc(ftTotal);
  230. end;
  231.  
  232. function FileTree.Compare(Key1, Key2 : Pointer) : CompareType;
  233.   {-Compare two keys, returning Less, Equal, Greater}
  234. begin
  235.   Compare := CompString(StringPtr(Key1)^, StringPtr(Key2)^);
  236. end;
  237.  
  238. function FileTree.GetKey(N : TreeNodePtr) : Pointer;
  239.   {-Return a pointer to the key value for node N}
  240. begin
  241.   GetKey := FileNodePtr(N)^.NameP;
  242. end;
  243.  
  244. function DateTimeStr(DT : LongInt) : String;
  245.   {-Return a formatted date-time string}
  246. type
  247.   String2 = String[2];
  248. var
  249.   T : DateTime;
  250.  
  251.   function W2S2(W : Word) : String2;
  252.   var
  253.     S : String2;
  254.   begin
  255.     Str((W mod 100):2, S);
  256.     if S[1] = ' ' then
  257.       S[1] := '0';
  258.     W2S2 := S;
  259.   end;
  260.  
  261. begin
  262.   UnpackTime(DT, T);
  263.   with T do
  264.     DateTimeStr := W2S2(Year)+'/'+W2S2(Month)+'/'+W2S2(Day)+' '+
  265.                    W2S2(Hour)+':'+W2S2(Min)+':'+W2S2(Sec);
  266. end;
  267.  
  268. {$F+}
  269. procedure DumpNode(N : TreeNodePtr; T : TreePtr);
  270.   {-Dump one tree node}
  271. var
  272.   DNodeP : DirNodePtr;
  273. begin
  274.   if FileNodePtr(N)^.DirList.Size >= 2 then begin
  275.     {At least two instances of file, write the filename}
  276.     WriteLn(FileNodePtr(N)^.NameP^);
  277.     {Scan the list of directories}
  278.     DNodeP := DirNodePtr(FileNodePtr(N)^.DirList.Head);
  279.     while DNodeP <> nil do begin
  280.       WriteLn('  ', DNodeP^.Size:8, ' ', DateTimeStr(DNodeP^.Time), ' ',
  281.               StLocase(DNodeP^.DNameP^));
  282.       DNodeP := DirNodePtr(FileNodePtr(N)^.DirList.Next(DNodeP));
  283.     end;
  284.   end;
  285. end;
  286. {$F-}
  287.  
  288. procedure FileTree.DumpDups;
  289.   {-Dump files that appear at least Min times}
  290. begin
  291.   VisitNodesUp(DumpNode);
  292. end;
  293.  
  294. procedure FileTree.GetCounts(var Unique, Total : LongInt);
  295.   {-Return the number of unique names and the total number of names}
  296. begin
  297.   Unique := ftUnique;
  298.   Total := ftTotal;
  299. end;
  300.  
  301. constructor WildMatcher.Init(Mask : String; CaseSensitive : Boolean);
  302.   {-Initialize the mask string. May fail}
  303. begin
  304.   if not Root.Init then
  305.     Fail;
  306.   if Length(Mask) > 127 then
  307.     Fail;
  308.   if Pos(EndChar, Mask) <> 0 then
  309.     Fail;
  310.   maCase := CaseSensitive;
  311.   maMask := Mask;
  312.   SimplifyMask;
  313.   maMask[Length(maMask)+1] := EndChar;
  314. end;
  315.  
  316. function WildMatcher.Matches(Name : String) : Boolean;
  317.   {-Return True if Name matches Mask}
  318. var
  319.   NLen : Byte absolute Name;
  320.   MPos : Word;
  321.   NPos : Word;
  322.   MPSave : Word;
  323.   NPSave : Word;
  324.   AnyOn : Boolean;
  325.   Ch : Char;
  326. begin
  327.   Matches := False;
  328.  
  329.   {Add terminator to input string}
  330.   Name[NLen+1] := EndChar;
  331.  
  332.   AnyOn := False;
  333.   MPos := 1;
  334.   NPos := 1;
  335.  
  336.   while (maMask[MPos] <> EndChar) or (Name[NPos] <> EndChar) do begin
  337.     {Look for '*'}
  338.     if maMask[MPos] = AnyChar then begin
  339.       if MPos >= Length(maMask) then begin
  340.         {Last character in maMask is '*', rest must match}
  341.         Matches := True;
  342.         Exit;
  343.       end;
  344.       AnyOn := True;
  345.       NPSave := NPos;
  346.       inc(MPos);
  347.       MPSave := MPos;
  348.     end;
  349.  
  350.     {Get next character from Name string}
  351.     if maCase then
  352.       Ch := Name[NPos]
  353.     else
  354.       Ch := UpCase(Name[NPos]);
  355.  
  356.     {Look for literal match}
  357.     if (Ch <> EndChar) and ((maMask[MPos] = OneChar) or (maMask[MPos] = Ch))
  358.     then begin
  359.       {Matching character}
  360.       inc(MPos);
  361.       inc(NPos);
  362.     end else begin
  363.       {Mismatched character}
  364.       if not AnyOn or (NPSave >= Length(Name)) then
  365.         {Fatal mismatch, no '*' in effect or no way to advance past mismatch}
  366.         Exit;
  367.       {Increment restart point}
  368.       inc(NPSave);
  369.       {Try again at next Name position}
  370.       NPos := NPSave;
  371.       {Restart maMask just after the '*'}
  372.       MPos := MPSave;
  373.     end;
  374.   end;
  375.  
  376.   Matches := True;
  377. end;
  378.  
  379. function WildMatcher.GetMask : String;
  380.   {-Return the simplified mask}
  381. begin
  382.   GetMask := maMask;
  383. end;
  384.  
  385. procedure WildMatcher.SimplifyMask;
  386.   {-Used internally to simplify mask when object instantiated}
  387. var
  388.   MLen : Byte;
  389.   MPos : Word;
  390.   OMask : String;
  391.   OLen : Byte absolute OMask;
  392. begin
  393.   MLen := Length(maMask);
  394.   MPos := 1;
  395.   OLen := 0;
  396.   while MPos <= MLen do begin
  397.     if (MPos = 1) or (maMask[MPos] <> '*') or (maMask[MPos-1] <> '*') then begin
  398.       {Transfer maMask to OMask, skipping repeated asterisks}
  399.       inc(OLen);
  400.       OMask[OLen] := maMask[MPos];
  401.       if not maCase then
  402.         OMask[OLen] := UpCase(OMask[OLen]);
  403.     end;
  404.     inc(MPos);
  405.   end;
  406.   maMask := OMask;
  407. end;
  408.  
  409. procedure ScanDir(Dir : PathStr);
  410.   {-Scan one directory}
  411. var
  412.   FRec : SearchRec;
  413.  
  414.   procedure WriteStatus;
  415.   begin
  416.     Write(StdErr, Dir);
  417.   end;
  418.  
  419.   procedure ClearStatus;
  420.   begin
  421.     Write(StdErr, ^M, CharStr(' ', Length(Dir)), ^M);
  422.   end;
  423.  
  424. begin
  425.   WriteStatus;
  426.   FindFirst(AddBackSlash(Dir)+'*.*', FileAttr, FRec);
  427.   while DosError = 0 do begin
  428.     if (FRec.Attr and VolumeID) <> 0 then
  429.       {do nothing for volume labels}
  430.     else if (FRec.Attr and Directory <> 0) then begin
  431.       {a directory, look deeper}
  432.       if (FRec.Name <> '.') and (FRec.Name <> '..') then begin
  433.         ClearStatus;
  434.         ScanDir(AddBackSlash(Dir)+FRec.Name);
  435.         WriteStatus;
  436.       end;
  437.     end else if DefaultMask or FileMask.Matches(FRec.Name) then
  438.       {a matching file, add it to FileTree}
  439.       FileNames.FileInsert(FRec.Name, FRec.Time, FRec.Size, Dir);
  440.     FindNext(FRec);
  441.   end;
  442.   ClearStatus;
  443. end;
  444.  
  445. procedure ScanDrive(DriveLet : Char);
  446.   {-Scan one drive for duplicate files. DriveLet assumed to be valid}
  447. begin
  448.   ScanDir(DriveLet+':\');
  449. end;
  450.  
  451. function IsOption(var Param : Word) : Boolean;
  452.   {-Return True if ParamStr(Param) is an option, and evaluate it if so}
  453. var
  454.   Arg : String[127];
  455. begin
  456.   IsOption := False;
  457.   Arg := ParamStr(Param);
  458.   case Arg[1] of
  459.     '/', '-' :
  460.       if Length(Arg) <> 2 then
  461.         Abort('Invalid option: '+Arg)
  462.       else
  463.         case UpCase(Arg[2]) of
  464.           'S' :
  465.              if Param = ParamCount then
  466.                Abort('Missing parameter after: '+Arg)
  467.              else begin
  468.                inc(Param);
  469.                Arg := ParamStr(Param);
  470.                {Validate mask}
  471.                if (Length(Arg) > 12) or
  472.                   (JustFileName(Arg) <> Arg) or
  473.                   (Pos('.', Arg) > 9) then
  474.                  Abort('Invalid file mask: '+Arg);
  475.                FileMask.Init(StUpcase(Arg), True);
  476.                DefaultMask := (FileMask.GetMask = '*.*');
  477.                IsOption := True;
  478.              end;
  479.         else
  480.           Abort('Invalid option: '+Arg);
  481.         end;
  482.   end;
  483. end;
  484.  
  485. function IsValidDrive(DriveName : String) : Boolean;
  486.   {-Return true if DriveName specifies a valid drive}
  487. begin
  488.   IsValidDrive := False;
  489.   case Length(DriveName) of
  490.     1 : {OK so far};
  491.     2 : {Assure second character is a colon}
  492.      if DriveName[2] <> ':' then
  493.        Exit;
  494.   else
  495.     Exit;
  496.   end;
  497.   IsValidDrive := ValidDrive(Upcase(DriveName[1]));
  498. end;
  499.  
  500. procedure ValidateDrives;
  501.   {-Assure the requested drives are valid}
  502. var
  503.   Param : Word;
  504. begin
  505.   Param := 1;
  506.   while Param <= ParamCount do begin
  507.     if not IsOption(Param) then
  508.       if not IsValidDrive(ParamStr(Param)) then
  509.         Abort('Invalid drive: '+ParamStr(Param));
  510.     inc(Param);
  511.   end;
  512. end;
  513.  
  514. procedure ScanDrives;
  515.   {-Scan the requested drives for duplicate files}
  516. var
  517.   Param : Word;
  518.   DriveLet : String[1];
  519. begin
  520.   Param := 1;
  521.   while Param <= ParamCount do begin
  522.     if not IsOption(Param) then begin
  523.       DriveLet := Copy(ParamStr(Param), 1, 1); {Minimize stack usage}
  524.       ScanDrive(UpCase(DriveLet[1]));
  525.     end;
  526.     inc(Param);
  527.   end;
  528. end;
  529.  
  530. begin
  531.   {StdErr will be used for messages and status}
  532.   if not OpenStdDev(StdErr, StdErrHandle) then
  533.     Halt;
  534.  
  535.   {Write copyright and help message}
  536.   WriteCopyRight;
  537.   if ParamCount = 0 then
  538.     WriteHelp;
  539.  
  540.   {The FileNames tree object will store the filenames and their locations}
  541.   FileNames.Init;
  542.  
  543.   {Scan all files by default}
  544.   DefaultMask := True;
  545.  
  546.   {Validate the requested drives before scanning anything}
  547.   ValidateDrives;
  548.  
  549.   {Scan the requested drives}
  550.   ScanDrives;
  551.  
  552.   {Dump the output}
  553.   FileNames.DumpDups;
  554.  
  555.   Close(StdErr);
  556. end.
  557.